--- id: TASK-007 title: 'custard CLI: preview / promote / release (Charm spinner)' status: "\U0001F3C1 Done" assignee: [] created_date: '2026-06-18 06:36' updated_date: '2026-06-18 15:25' labels: - feature dependencies: [] priority: medium ordinal: 7000 --- ## Description A Charm-style CLI that owns DEPLOY + RELEASE — not git push (you keep committing/pushing via lazygit/Claude; deploy is decoupled since 'vercel deploy' uploads the local dir, not the remote). Foreground + inline spinner per stage, so no notification system needed. Reads .custard.yaml for check + deploy config. Verbs: (1) preview — run checks, then vercel deploy (preview), print URL; (2) promote — vercel promote the latest/chosen preview to prod (same build, no rebuild); (3) release vX.Y.Z — run checks, tag + push the tag so the existing brew webhook auto-publishes. Supersedes the earlier server-side-CI sketch in this task's ACs. ## Acceptance Criteria - [ ] #1 CLI reads .custard.yaml: ci check commands + deploy (preview/prod) config - [ ] #2 promote: vercel promote latest preview (or 'promote ') → prod, no rebuild - [ ] #3 release vX.Y.Z: run checks, then tag + push tag → existing brew webhook publishes - [ ] #4 Charm UI (Bubble Tea/Lipgloss spinner); distributed via the self-hosted tap (dogfood) - [ ] #5 Does NOT perform git push — lazygit/Claude workflow unaffected - [ ] #6 Multiple previews before promote supported; promote defaults to most recent - [ ] #7 check verb: runs .custard.yaml checks on the working tree (dirty allowed) — fast inner loop, no deploy - [ ] #8 Status reporting: CLI POSTs signed {repo, commit, state(checked|preview|prod), url} to custard - [ ] #9 Forge badges: mark commits ✓ in production / 👁 preview / ✓ checked / ⚠ unverified so the live forge shows what's real vs in-flight - [ ] #10 preview: build a clean export of HEAD (git archive/worktree) → run checks → vercel deploy (preview) → print URL; spinner per stage, ✗ + captured output on failure - [ ] #11 Deploys build from the COMMIT, never the working dir — uncommitted edits are structurally excluded (never deployed), and a dirty tree does not block deploying committed work - [ ] #12 preview/promote require HEAD pushed to soft (deployed code == an in-repo commit the forge badge maps to); if unpushed, CLI says push first - [ ] #13 preview/release run checks against the EXPORTED HEAD (committed code); standalone check runs against the working tree — distinct by design - [ ] #14 Single 'custard' binary: 'serve' runs the server (droplet), client verbs (check/preview/promote/release) run locally; refactor cmd/custard into subcommands - [ ] #15 CLI config via ~/.custardrc (custard base URL + status token); vercel auth uses local vercel login - [ ] #16 Server: POST /status (HMAC-signed) persists {repo,commit,state,url} to a writable store (e.g. /var/lib/custard/state); systemd ReadWritePaths includes it - [ ] #17 promote tracks the last preview (local state, e.g. ~/.custard/state.json) or accepts an explicit url; errors clearly if no preview exists ## Implementation Notes Design detail / schema: .custard.yaml (per repo): brew: { enabled: true, package: "." } # TASK-006 (release) ci: # commands run in order; nonzero exit = fail - go vet ./... - go test ./... deploy: preview: vercel deploy # must print the preview URL on stdout # promote uses: vercel promote (no rebuild) Verbs (one binary, 'custard '): check run ci on WORKING TREE (dirty ok); no deploy; fast inner loop preview export HEAD (git archive→tmp) → run ci on export → deploy.preview → capture+print URL → POST /status(preview) promote [url] vercel promote last/given preview → prod → POST /status(prod) release vX.Y.Z run ci on HEAD → tag + push tag (TASK-006 webhook publishes brew) → POST /status Gates: preview/promote do NOT block on a dirty tree (they build from the commit, so uncommitted edits are excluded); they DO require HEAD pushed to soft so deployed==in-repo and the badge maps. check has no gate. Server additions: POST /status (HMAC, STATUS_SECRET in /etc/custard.env) → persist per (repo,commit); forge reads it to badge commits/branch as: ✓ in production / 👁 preview / ✓ checked / ⚠ unverified. State store writable (ReadWritePaths). Distribution: CLI ships via the self-hosted tap (custard formula) — dogfoods TASK-006. Open: status-store backend (flat json vs sqlite); whether release should also require clean tree.